0135. 字节流(Byte Streams)vs 普通流
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 🤔 字节流与普通流在数据处理上有什么本质区别 ?
- 4. 🤔 什么场景下应该优先使用字节流 ?
- 5. 🤔 如何创建一个字节流 ?
- 6. 🤔 ReadableStreamBYOBReader 的 BYOB 是什么意思 ?
- 7. 💻 demos.1 - 创建并读取一个字节流
- 8. 💻 demos.2 - 对比字节流与普通流的性能差异
- 9. 🔗 引用
1. 🎯 本节内容
- 字节流与普通流的核心差异
- ReadableStreamBYOBReader 读取器
- Uint8Array 类型的 chunk 数据
- 字节流的应用场景
- autoAllocateChunkSize 参数
- 字节流的性能优势
2. 🫧 评价
字节流通过 type: 'bytes' 标识,专为二进制数据优化,支持 BYOB Reader 实现零拷贝读取。与普通流相比,字节流在处理文件、网络数据等二进制场景下内存效率更高,但限制了数据类型为 ArrayBufferView。理解两者差异和适用场景,是选择正确流类型的关键。
3. 🤔 字节流与普通流在数据处理上有什么本质区别 ?
字节流限定数据类型为 ArrayBufferView,支持零拷贝读取;普通流可传输任意类型数据。
3.1. 数据类型限制
js
// 普通流:可以传输任意类型
const normalStream = new ReadableStream({
start(controller) {
controller.enqueue('string') // ✅ 字符串
controller.enqueue({ id: 1 }) // ✅ 对象
controller.enqueue([1, 2, 3]) // ✅ 数组
controller.enqueue(new Uint8Array(10)) // ✅ 二进制
},
})
// 字节流:只能传输 ArrayBufferView
const byteStream = new ReadableStream({
type: 'bytes',
start(controller) {
controller.enqueue(new Uint8Array(10)) // ✅ Uint8Array
controller.enqueue(new Uint16Array(5)) // ✅ Uint16Array
controller.enqueue(new DataView(new ArrayBuffer(8))) // ✅ DataView
// controller.enqueue('string') // ❌ TypeError
// controller.enqueue({ data: [] }) // ❌ TypeError
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
3.2. Reader 类型差异
js
// 普通流:只能使用默认 Reader
const reader1 = normalStream.getReader()
console.log(reader1.constructor.name) // ReadableStreamDefaultReader
// 字节流:支持两种 Reader
const defaultReader = byteStream.getReader()
console.log(defaultReader.constructor.name) // ReadableStreamDefaultReader
const byobReader = byteStream.getReader({ mode: 'byob' })
console.log(byobReader.constructor.name) // ReadableStreamBYOBReader1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
3.3. 内存管理差异
js
// 普通流:数据拷贝
const normal = new ReadableStream({
pull(controller) {
const data = new Uint8Array(1024)
// 数据被拷贝到内部队列
controller.enqueue(data)
// data 可以立即复用或修改
data[0] = 255
},
})
// 字节流 + BYOB Reader:零拷贝
const byte = new ReadableStream({
type: 'bytes',
pull(controller) {
const view = controller.byobRequest?.view
if (view) {
// 直接写入用户提供的缓冲区
view[0] = 100
controller.byobRequest.respond(view.byteLength)
}
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
3.4. 对比表格
| 特性 | 普通流 | 字节流 |
|---|---|---|
| type 参数 | 默认(不指定) | 'bytes' |
| 数据类型 | 任意类型 | ArrayBufferView(Uint8Array 等) |
| Reader 类型 | DefaultReader | DefaultReader 或 BYOBReader |
| 零拷贝支持 | 不支持 | 支持(BYOB Reader) |
| 内存效率 | 中等 | 高 |
| 适用场景 | 对象流、消息流 | 文件流、网络流、二进制数据 |
| 队列策略默认 | CountQueuingStrategy | ByteLengthQueuingStrategy |
3.5. Controller 接口差异
js
// 普通流的 controller
const normalController = {
enqueue(chunk) {}, // chunk 可以是任意类型
close() {},
error(reason) {},
desiredSize: 1,
}
// 字节流的 controller
const byteController = {
enqueue(chunk) {}, // chunk 必须是 ArrayBufferView
close() {},
error(reason) {},
desiredSize: 16384, // 默认 highWaterMark 更大
byobRequest: {
// 仅字节流有
view: Uint8Array,
respond(bytesWritten) {},
respondWithNewView(view) {},
},
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
本质区别在于字节流为二进制数据提供了专门优化,牺牲灵活性换取性能。
4. 🤔 什么场景下应该优先使用字节流 ?
处理文件、网络数据、大型二进制数据时优先使用字节流。
4.1. 典型使用场景
js
// 场景 1:文件读取
async function readFile(file) {
const stream = file.stream() // 浏览器 File API 返回字节流
const reader = stream.getReader({ mode: 'byob' })
const buffer = new Uint8Array(64 * 1024) // 64KB
const { value } = await reader.read(buffer)
return value // 零拷贝读取
}
// 场景 2:网络请求
const response = await fetch('/large-file.bin')
const byteStream = response.body // ReadableStream<Uint8Array>
// 场景 3:视频/音频流
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
const videoTrack = stream.getVideoTracks()[0]
const reader = new MediaStreamTrackProcessor({
track: videoTrack,
}).readable.getReader()
// 处理视频帧(二进制数据)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4.2. 场景决策树
| 数据特征 | 推荐流类型 | 原因 |
|---|---|---|
| 文件上传/下载 | 字节流 | 大量二进制,需内存优化 |
| WebSocket 二进制消息 | 字节流 | ArrayBuffer/Blob 数据 |
| JSON API 响应 | 普通流 | 对象数据,无需字节级控制 |
| 日志流 | 普通流 | 字符串/对象,无性能要求 |
| 视频/音频编解码 | 字节流 | 大量帧数据,内存敏感 |
| 数据库查询结果 | 普通流 | 行对象,非二进制 |
| 图片处理管道 | 字节流 | 像素数据,需高性能 |
| 事件流(SSE) | 普通流 | 文本消息 |
4.3. 实际示例对比
js
// ❌ 不推荐:用普通流处理大文件
async function uploadFileBad(file) {
const stream = new ReadableStream({
async start(controller) {
const arrayBuffer = await file.arrayBuffer() // 一次性加载到内存
controller.enqueue(new Uint8Array(arrayBuffer))
controller.close()
},
})
// 问题:大文件会导致内存爆炸
}
// ✅ 推荐:用字节流分块处理
function uploadFileGood(file) {
return file.stream() // 浏览器原生字节流,按需读取
}
// ❌ 不推荐:用字节流处理 JSON
const jsonStream = new ReadableStream({
type: 'bytes', // 不必要的限制
start(controller) {
const data = JSON.stringify({ id: 1 })
controller.enqueue(new TextEncoder().encode(data)) // 额外编码开销
controller.close()
},
})
// ✅ 推荐:用普通流处理 JSON
const jsonStreamGood = new ReadableStream({
start(controller) {
controller.enqueue({ id: 1 }) // 直接传输对象
controller.close()
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
4.4. 性能考量
js
// 小数据量(< 1MB):普通流足够
const smallData = new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(1024))
controller.close()
},
})
// 大数据量(> 10MB):字节流 + BYOB 优化
function createLargeFileStream(file) {
return new ReadableStream({
type: 'bytes',
async pull(controller) {
const request = controller.byobRequest
if (request) {
// 零拷贝写入用户缓冲区
const bytesRead = await readInto(request.view)
request.respond(bytesRead)
}
},
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
4.5. 兼容性考虑
js
// 检测 BYOB Reader 支持
function supportsBYOB() {
try {
const stream = new ReadableStream({ type: 'bytes' })
const reader = stream.getReader({ mode: 'byob' })
reader.cancel()
return true
} catch {
return false
}
}
// 降级方案
function createOptimalStream(data) {
if (supportsBYOB() && data instanceof ArrayBuffer) {
return new ReadableStream({
type: 'bytes',
start(controller) {
controller.enqueue(new Uint8Array(data))
controller.close()
},
})
}
// 回退到普通流
return new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array(data))
controller.close()
},
})
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
核心原则:处理二进制且关注性能用字节流,处理对象或小数据用普通流。
5. 🤔 如何创建一个字节流 ?
在 ReadableStream 构造函数中设置 type: 'bytes'。
5.1. 基本创建方式
js
const byteStream = new ReadableStream({
type: 'bytes', // 关键:标识为字节流
start(controller) {
console.log('字节流初始化')
},
pull(controller) {
// 生产数据
const chunk = new Uint8Array(1024)
controller.enqueue(chunk)
},
cancel(reason) {
console.log('字节流取消:', reason)
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
5.2. 使用 autoAllocateChunkSize 参数
js
const autoByteStream = new ReadableStream(
{
type: 'bytes',
pull(controller) {
// 当使用默认 Reader 时,系统自动分配缓冲区
const chunk = new Uint8Array(controller.desiredSize)
fillRandomData(chunk)
controller.enqueue(chunk)
},
},
{
// 自动分配的块大小
autoAllocateChunkSize: 64 * 1024, // 64KB
}
)
// 使用默认 Reader
const reader = autoByteStream.getReader()
const { value } = await reader.read()
console.log(value.byteLength) // 64KB1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
5.3. 响应 byobRequest
js
const responsiveStream = new ReadableStream({
type: 'bytes',
pull(controller) {
const byobRequest = controller.byobRequest
if (byobRequest) {
// BYOB Reader 提供了缓冲区
const view = byobRequest.view
console.log('写入用户缓冲区,大小:', view.byteLength)
// 直接写入
for (let i = 0; i < view.byteLength; i++) {
view[i] = Math.floor(Math.random() * 256)
}
byobRequest.respond(view.byteLength)
} else {
// 默认 Reader,自己分配
const chunk = new Uint8Array(1024)
controller.enqueue(chunk)
}
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
5.4. 实战:文件流包装
js
function createFileStream(file) {
let offset = 0
const chunkSize = 64 * 1024
return new ReadableStream({
type: 'bytes',
async pull(controller) {
if (offset >= file.size) {
controller.close()
return
}
const byobRequest = controller.byobRequest
if (byobRequest) {
// BYOB 模式:直接读到用户缓冲区
const view = byobRequest.view
const slice = file.slice(offset, offset + view.byteLength)
const buffer = await slice.arrayBuffer()
new Uint8Array(view.buffer, view.byteOffset, view.byteLength).set(
new Uint8Array(buffer)
)
offset += buffer.byteLength
byobRequest.respond(buffer.byteLength)
} else {
// 默认模式:自己分配缓冲区
const slice = file.slice(offset, offset + chunkSize)
const buffer = await slice.arrayBuffer()
controller.enqueue(new Uint8Array(buffer))
offset += buffer.byteLength
}
},
})
}
// 使用
const fileInput = document.querySelector('input[type="file"]')
const file = fileInput.files[0]
const stream = createFileStream(file)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
5.5. 错误处理
js
const errorProneStream = new ReadableStream({
type: 'bytes',
pull(controller) {
try {
const data = readSomeData()
// ✅ 正确:检查数据类型
if (!(data instanceof Uint8Array)) {
throw new TypeError('字节流只能入队 ArrayBufferView')
}
controller.enqueue(data)
} catch (error) {
controller.error(error)
}
},
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5.6. 使用队列策略
js
const strategicStream = new ReadableStream(
{
type: 'bytes',
pull(controller) {
controller.enqueue(new Uint8Array(1024))
},
},
new ByteLengthQueuingStrategy({
highWaterMark: 128 * 1024, // 128KB 缓冲
})
)
console.log(strategicStream.constructor.name) // ReadableStream1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
创建字节流的关键是 type: 'bytes',其他参数与普通流类似。
6. 🤔 ReadableStreamBYOBReader 的 BYOB 是什么意思 ?
BYOB 是 Bring Your Own Buffer 的缩写,表示用户提供缓冲区进行读取。
6.1. BYOB 的含义
js
// 默认 Reader:系统分配缓冲区
const defaultReader = stream.getReader()
const { value } = await defaultReader.read()
// value 是系统创建的 Uint8Array
// BYOB Reader:用户提供缓冲区
const byobReader = stream.getReader({ mode: 'byob' })
const buffer = new Uint8Array(1024) // 用户自己分配
const { value } = await byobReader.read(buffer)
// value 是用户提供的 buffer(或其切片)1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
6.2. 零拷贝优势
js
// 传统方式:多次内存拷贝
async function readTraditional(stream) {
const reader = stream.getReader()
const chunks = []
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value) // 拷贝 1:临时数组
}
// 拷贝 2:合并数组
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0)
const result = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
result.set(chunk, offset) // 拷贝 3:设置数据
offset += chunk.byteLength
}
return result
}
// BYOB 方式:零拷贝
async function readBYOB(stream) {
const reader = stream.getReader({ mode: 'byob' })
const buffer = new Uint8Array(64 * 1024) // 一次性分配
const { value } = await reader.read(buffer)
// 数据直接写入 buffer,无拷贝
return value
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
6.3. 缓冲区复用
js
async function readWithReuse(stream) {
const reader = stream.getReader({ mode: 'byob' })
const buffer = new Uint8Array(4096) // 4KB 缓冲区
while (true) {
const { done, value } = await reader.read(buffer)
if (done) break
processChunk(value)
// ⚠️ 如果 value 是完整 buffer,可以复用
// 如果是切片,需重新分配
if (value.byteLength < buffer.byteLength) {
buffer = new Uint8Array(4096) // 重新分配
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
6.4. 完整示例
js
// 创建支持 BYOB 的字节流
const stream = new ReadableStream({
type: 'bytes',
pull(controller) {
const request = controller.byobRequest
if (request) {
// BYOB Reader 提供了缓冲区
const view = request.view
// 模拟读取数据到用户缓冲区
for (let i = 0; i < view.byteLength; i++) {
view[i] = Math.floor(Math.random() * 256)
}
request.respond(view.byteLength)
}
},
})
// 使用 BYOB Reader
const reader = stream.getReader({ mode: 'byob' })
// 读取方式 1:提供缓冲区
const myBuffer = new Uint8Array(1024)
const result1 = await reader.read(myBuffer)
console.log('方式 1:', result1.value === myBuffer) // 可能为 true
// 读取方式 2:使用返回的 value 作为下次输入
let buffer = new Uint8Array(1024)
while (true) {
const { done, value } = await reader.read(buffer)
if (done) break
console.log('读取了', value.byteLength, '字节')
buffer = value // 复用返回的缓冲区
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
6.5. BYOB 的限制
js
// ❌ 错误:普通流不支持 BYOB
const normalStream = new ReadableStream({
// 没有 type: 'bytes'
})
try {
const reader = normalStream.getReader({ mode: 'byob' })
} catch (error) {
console.log(error.message) // This stream does not support BYOB readers
}
// ❌ 错误:传入非 ArrayBufferView
const byteStream = new ReadableStream({ type: 'bytes' })
const reader = byteStream.getReader({ mode: 'byob' })
try {
await reader.read('not a buffer')
} catch (error) {
console.log(error.message) // TypeError
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
6.6. 性能对比
js
// 测试:读取 10MB 数据
async function benchmark() {
const size = 10 * 1024 * 1024
// 方式 1:默认 Reader
const stream1 = createMockStream(size)
const start1 = performance.now()
await consumeWithDefault(stream1)
console.log('默认 Reader:', performance.now() - start1, 'ms')
// 方式 2:BYOB Reader
const stream2 = createMockStream(size)
const start2 = performance.now()
await consumeWithBYOB(stream2)
console.log('BYOB Reader:', performance.now() - start2, 'ms')
}
async function consumeWithBYOB(stream) {
const reader = stream.getReader({ mode: 'byob' })
let buffer = new Uint8Array(64 * 1024)
while (true) {
const { done, value } = await reader.read(buffer)
if (done) break
buffer = value
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
BYOB 通过让用户控制缓冲区分配,实现零拷贝和内存复用,显著提升大数据量场景性能。
7. 💻 demos.1 - 创建并读取一个字节流
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>创建并读取字节流</title>
<style>
body {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
}
.demo {
margin: 20px 0;
padding: 15px;
border: 1px solid #ccc;
}
.log {
border: 1px solid #ddd;
padding: 10px;
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
background: #f9f9f9;
}
.controls {
margin: 10px 0;
}
button {
margin-right: 10px;
}
.hex-dump {
background: #263238;
color: #aed581;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 11px;
overflow-x: auto;
}
</style>
</head>
<body>
<h1>字节流创建与读取</h1>
<div class="demo">
<h3>Demo 1:基本字节流</h3>
<p>创建一个简单的字节流,使用默认 Reader 读取</p>
<button onclick="demo1()">运行</button>
<button onclick="clearLog('log1')">清空</button>
<div class="log" id="log1"></div>
</div>
<div class="demo">
<h3>Demo 2:使用 BYOB Reader</h3>
<p>用户提供缓冲区进行零拷贝读取</p>
<button onclick="demo2()">运行</button>
<button onclick="clearLog('log2')">清空</button>
<div class="log" id="log2"></div>
</div>
<div class="demo">
<h3>Demo 3:读取文件为字节流</h3>
<div class="controls">
<input type="file" id="fileInput" />
<button onclick="demo3()">读取文件</button>
<button onclick="clearLog('log3')">清空</button>
</div>
<div class="log" id="log3"></div>
<div class="hex-dump" id="hexDump"></div>
</div>
<script src="1.js"></script>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
js
function log(id, message) {
const logEl = document.getElementById(id)
logEl.innerHTML += `${message}\n`
logEl.scrollTop = logEl.scrollHeight
}
function clearLog(id) {
document.getElementById(id).innerHTML = ''
if (id === 'log3') {
document.getElementById('hexDump').innerHTML = ''
}
}
// Demo 1:基本字节流
async function demo1() {
clearLog('log1')
log('log1', '创建字节流...\n')
let chunkCount = 0
const byteStream = new ReadableStream({
type: 'bytes',
start(controller) {
log('log1', '✅ 字节流已初始化')
log('log1', ` 初始 desiredSize: ${controller.desiredSize}\n`)
},
pull(controller) {
chunkCount++
if (chunkCount > 5) {
controller.close()
log('log1', '🔚 流已关闭\n')
return
}
// 生成随机数据
const size = 16
const chunk = new Uint8Array(size)
for (let i = 0; i < size; i++) {
chunk[i] = Math.floor(Math.random() * 256)
}
log('log1', `📤 pull() #${chunkCount}:`)
log('log1', ` 生成 ${size} 字节数据`)
log('log1', ` 数据: [${Array.from(chunk.slice(0, 8)).join(', ')}...]`)
controller.enqueue(chunk)
log('log1', ` 入队后 desiredSize: ${controller.desiredSize}\n`)
},
})
log('log1', '开始读取(使用默认 Reader)...\n')
const reader = byteStream.getReader()
log('log1', `Reader 类型: ${reader.constructor.name}\n`)
let totalBytes = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
totalBytes += value.byteLength
log('log1', `📥 读取到 ${value.byteLength} 字节`)
log('log1', ` 数据: [${Array.from(value.slice(0, 8)).join(', ')}...]\n`)
}
log('log1', `✅ 完成,共读取 ${totalBytes} 字节`)
}
// Demo 2:使用 BYOB Reader
async function demo2() {
clearLog('log2')
log('log2', '创建支持 BYOB 的字节流...\n')
const byteStream = new ReadableStream({
type: 'bytes',
pull(controller) {
const byobRequest = controller.byobRequest
if (byobRequest) {
log('log2', '🎯 检测到 BYOB Request:')
log('log2', ` 用户缓冲区大小: ${byobRequest.view.byteLength} 字节`)
const view = byobRequest.view
// 直接写入用户提供的缓冲区
for (let i = 0; i < view.byteLength; i++) {
view[i] = i % 256
}
log('log2', ` 已填充 ${view.byteLength} 字节\n`)
byobRequest.respond(view.byteLength)
} else {
log('log2', '⚠️ 没有 BYOB Request,使用默认方式\n')
controller.enqueue(new Uint8Array(32))
}
},
})
log('log2', '使用 BYOB Reader 读取...\n')
const reader = byteStream.getReader({ mode: 'byob' })
log('log2', `Reader 类型: ${reader.constructor.name}\n`)
// 第一次读取:提供 64 字节缓冲区
log('log2', '📖 第一次读取(64 字节缓冲区):')
const buffer1 = new Uint8Array(64)
const result1 = await reader.read(buffer1)
log('log2', ` done: ${result1.done}`)
log('log2', ` 读取字节数: ${result1.value.byteLength}`)
log('log2', ` 缓冲区是否相同: ${result1.value.buffer === buffer1.buffer}`)
log(
'log2',
` 数据样本: [${Array.from(result1.value.slice(0, 10)).join(', ')}...]\n`
)
// 第二次读取:提供 128 字节缓冲区
log('log2', '📖 第二次读取(128 字节缓冲区):')
const buffer2 = new Uint8Array(128)
const result2 = await reader.read(buffer2)
log('log2', ` 读取字节数: ${result2.value.byteLength}`)
log(
'log2',
` 数据样本: [${Array.from(result2.value.slice(0, 10)).join(', ')}...]\n`
)
reader.cancel()
log('log2', '✅ 完成')
}
// Demo 3:读取文件为字节流
async function demo3() {
clearLog('log3')
const fileInput = document.getElementById('fileInput')
const file = fileInput.files[0]
if (!file) {
log('log3', '❌ 请先选择文件')
return
}
log('log3', `文件信息:`)
log('log3', ` 名称: ${file.name}`)
log('log3', ` 大小: ${file.size} 字节`)
log('log3', ` 类型: ${file.type || '未知'}\n`)
log('log3', '创建文件字节流...\n')
const stream = file.stream()
log('log3', `Stream 类型: ${stream.constructor.name}`)
// 尝试使用 BYOB Reader
try {
const reader = stream.getReader({ mode: 'byob' })
log('log3', `Reader 类型: ${reader.constructor.name}\n`)
log('log3', '读取前 512 字节...\n')
const buffer = new Uint8Array(512)
const { value, done } = await reader.read(buffer)
if (done) {
log('log3', '⚠️ 文件为空')
return
}
log('log3', `✅ 成功读取 ${value.byteLength} 字节\n`)
// 生成十六进制转储
const hexDump = createHexDump(value)
document.getElementById('hexDump').textContent = hexDump
// 检测文件类型
const fileType = detectFileType(value)
log('log3', `文件类型检测: ${fileType}`)
reader.cancel()
} catch (error) {
log('log3', `❌ 错误: ${error.message}`)
}
}
function createHexDump(bytes) {
const maxBytes = Math.min(bytes.byteLength, 256)
let dump = 'Offset 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII\n'
dump +=
'------ ----------------------------------------------- ----------------\n'
for (let i = 0; i < maxBytes; i += 16) {
const offset = i.toString(16).padStart(6, '0').toUpperCase()
let hex = ''
let ascii = ''
for (let j = 0; j < 16; j++) {
if (i + j < maxBytes) {
const byte = bytes[i + j]
hex += byte.toString(16).padStart(2, '0').toUpperCase() + ' '
ascii += byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : '.'
} else {
hex += ' '
ascii += ' '
}
}
dump += `${offset} ${hex} ${ascii}\n`
}
if (bytes.byteLength > maxBytes) {
dump += `\n... (剩余 ${bytes.byteLength - maxBytes} 字节)\n`
}
return dump
}
function detectFileType(bytes) {
const signatures = {
PNG: [0x89, 0x50, 0x4e, 0x47],
JPEG: [0xff, 0xd8, 0xff],
GIF: [0x47, 0x49, 0x46],
PDF: [0x25, 0x50, 0x44, 0x46],
ZIP: [0x50, 0x4b, 0x03, 0x04],
}
for (const [type, signature] of Object.entries(signatures)) {
if (matchesSignature(bytes, signature)) {
return type
}
}
return '未知'
}
function matchesSignature(bytes, signature) {
if (bytes.length < signature.length) return false
return signature.every((byte, i) => bytes[i] === byte)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
8. 💻 demos.2 - 对比字节流与普通流的性能差异
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>字节流与普通流性能对比</title>
<style>
body {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
}
.demo {
margin: 20px 0;
padding: 15px;
border: 1px solid #ccc;
}
.controls {
margin: 15px 0;
padding: 10px;
background: #f5f5f5;
}
.results {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.result-card {
border: 1px solid #ddd;
padding: 15px;
background: white;
}
.result-card h4 {
margin-top: 0;
color: #333;
}
.metric {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.metric:last-child {
border-bottom: none;
}
.metric-label {
font-weight: 500;
}
.metric-value {
font-family: monospace;
color: #1976d2;
}
.winner {
background: #e8f5e9;
border-left: 3px solid #4caf50;
}
.log {
border: 1px solid #ddd;
padding: 10px;
margin-top: 10px;
max-height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 11px;
background: #f9f9f9;
}
button {
padding: 8px 16px;
margin-right: 10px;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>字节流 vs 普通流性能对比</h1>
<div class="demo">
<h3>性能基准测试</h3>
<div class="controls">
<label>
数据量(MB):
<input type="number" id="dataSize" value="10" min="1" max="100" />
</label>
<label>
块大小(KB):
<input type="number" id="chunkSize" value="64" min="1" max="1024" />
</label>
<button id="runBtn" onclick="runBenchmark()">运行测试</button>
<button onclick="clearResults()">清空结果</button>
</div>
<div class="results" id="results"></div>
<div class="log" id="log"></div>
</div>
<div class="demo">
<h3>内存使用对比</h3>
<p>测试大文件处理时的内存占用差异</p>
<button onclick="memoryTest()">运行内存测试</button>
<div class="log" id="memoryLog"></div>
</div>
<script src="1.js"></script>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
js
function log(id, message) {
const logEl = document.getElementById(id)
const time = new Date().toLocaleTimeString()
logEl.innerHTML += `[${time}] ${message}\n`
logEl.scrollTop = logEl.scrollHeight
}
function clearResults() {
document.getElementById('results').innerHTML = ''
document.getElementById('log').innerHTML = ''
}
// 运行基准测试
async function runBenchmark() {
clearResults()
const btn = document.getElementById('runBtn')
btn.disabled = true
const dataSizeMB = parseInt(document.getElementById('dataSize').value)
const chunkSizeKB = parseInt(document.getElementById('chunkSize').value)
const dataSize = dataSizeMB * 1024 * 1024
const chunkSize = chunkSizeKB * 1024
log('log', `开始测试: ${dataSizeMB}MB 数据, ${chunkSizeKB}KB 块大小\n`)
// 测试 1:普通流 + 默认 Reader
log('log', '测试 1: 普通流 + 默认 Reader...')
const result1 = await testNormalStream(dataSize, chunkSize)
log('log', `完成: ${result1.time.toFixed(2)}ms\n`)
// 测试 2:字节流 + 默认 Reader
log('log', '测试 2: 字节流 + 默认 Reader...')
const result2 = await testByteStreamDefault(dataSize, chunkSize)
log('log', `完成: ${result2.time.toFixed(2)}ms\n`)
// 测试 3:字节流 + BYOB Reader
log('log', '测试 3: 字节流 + BYOB Reader...')
const result3 = await testByteStreamBYOB(dataSize, chunkSize)
log('log', `完成: ${result3.time.toFixed(2)}ms\n`)
// 显示结果
displayResults([
{ name: '普通流 + 默认 Reader', ...result1 },
{ name: '字节流 + 默认 Reader', ...result2 },
{ name: '字节流 + BYOB Reader', ...result3 },
])
btn.disabled = false
log('log', '✅ 所有测试完成')
}
async function testNormalStream(totalSize, chunkSize) {
let bytesGenerated = 0
const stream = new ReadableStream({
pull(controller) {
if (bytesGenerated >= totalSize) {
controller.close()
return
}
const size = Math.min(chunkSize, totalSize - bytesGenerated)
const chunk = new Uint8Array(size)
controller.enqueue(chunk)
bytesGenerated += size
},
})
const startTime = performance.now()
let bytesRead = 0
let chunks = 0
const reader = stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
bytesRead += value.byteLength
chunks++
}
const time = performance.now() - startTime
const throughput = (bytesRead / 1024 / 1024 / (time / 1000)).toFixed(2)
return { time, throughput, chunks }
}
async function testByteStreamDefault(totalSize, chunkSize) {
let bytesGenerated = 0
const stream = new ReadableStream({
type: 'bytes',
pull(controller) {
if (bytesGenerated >= totalSize) {
controller.close()
return
}
const size = Math.min(chunkSize, totalSize - bytesGenerated)
const chunk = new Uint8Array(size)
controller.enqueue(chunk)
bytesGenerated += size
},
})
const startTime = performance.now()
let bytesRead = 0
let chunks = 0
const reader = stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
bytesRead += value.byteLength
chunks++
}
const time = performance.now() - startTime
const throughput = (bytesRead / 1024 / 1024 / (time / 1000)).toFixed(2)
return { time, throughput, chunks }
}
async function testByteStreamBYOB(totalSize, chunkSize) {
let bytesGenerated = 0
const stream = new ReadableStream({
type: 'bytes',
pull(controller) {
if (bytesGenerated >= totalSize) {
controller.close()
return
}
const request = controller.byobRequest
if (request) {
const size = Math.min(
request.view.byteLength,
totalSize - bytesGenerated
)
bytesGenerated += size
request.respond(size)
} else {
const size = Math.min(chunkSize, totalSize - bytesGenerated)
controller.enqueue(new Uint8Array(size))
bytesGenerated += size
}
},
})
const startTime = performance.now()
let bytesRead = 0
let chunks = 0
const reader = stream.getReader({ mode: 'byob' })
let buffer = new Uint8Array(chunkSize)
while (true) {
const { done, value } = await reader.read(buffer)
if (done) break
bytesRead += value.byteLength
chunks++
buffer = new Uint8Array(chunkSize) // 重新分配
}
const time = performance.now() - startTime
const throughput = (bytesRead / 1024 / 1024 / (time / 1000)).toFixed(2)
return { time, throughput, chunks }
}
function displayResults(results) {
const container = document.getElementById('results')
const fastest = results.reduce((min, r) => (r.time < min.time ? r : min))
results.forEach((result) => {
const isWinner = result === fastest
const card = document.createElement('div')
card.className = `result-card ${isWinner ? 'winner' : ''}`
card.innerHTML = `
<h4>${result.name} ${isWinner ? '🏆' : ''}</h4>
<div class="metric">
<span class="metric-label">总耗时</span>
<span class="metric-value">${result.time.toFixed(2)} ms</span>
</div>
<div class="metric">
<span class="metric-label">吞吐量</span>
<span class="metric-value">${result.throughput} MB/s</span>
</div>
<div class="metric">
<span class="metric-label">块数量</span>
<span class="metric-value">${result.chunks}</span>
</div>
<div class="metric">
<span class="metric-label">平均每块</span>
<span class="metric-value">${(result.time / result.chunks).toFixed(
3
)} ms</span>
</div>
`
container.appendChild(card)
})
}
// 内存测试
async function memoryTest() {
const logEl = document.getElementById('memoryLog')
logEl.innerHTML = ''
if (!performance.memory) {
log('memoryLog', '⚠️ 浏览器不支持 performance.memory API')
log(
'memoryLog',
'提示:在 Chrome 中使用 --enable-precise-memory-info 标志\n'
)
}
const sizeMB = 50
const size = sizeMB * 1024 * 1024
log('memoryLog', `测试场景: 读取 ${sizeMB}MB 数据\n`)
// 测试 1:普通流(累积所有数据)
log('memoryLog', '--- 测试 1: 普通流累积读取 ---')
await testMemoryNormal(size)
await sleep(1000)
// 测试 2:字节流 BYOB(复用缓冲区)
log('memoryLog', '\n--- 测试 2: 字节流 BYOB 复用缓冲区 ---')
await testMemoryBYOB(size)
log('memoryLog', '\n✅ 内存测试完成')
}
async function testMemoryNormal(totalSize) {
const before = getMemoryUsage()
log('memoryLog', `初始内存: ${before}MB`)
const chunks = []
const stream = new ReadableStream({
pull(controller) {
if (chunks.length * 64 * 1024 >= totalSize) {
controller.close()
return
}
controller.enqueue(new Uint8Array(64 * 1024))
},
})
const reader = stream.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value) // 保留所有块
}
const after = getMemoryUsage()
log('memoryLog', `读取后内存: ${after}MB`)
log('memoryLog', `内存增长: ${(after - before).toFixed(2)}MB`)
log('memoryLog', `累积块数: ${chunks.length}`)
}
async function testMemoryBYOB(totalSize) {
const before = getMemoryUsage()
log('memoryLog', `初始内存: ${before}MB`)
let bytesRead = 0
const stream = new ReadableStream({
type: 'bytes',
pull(controller) {
if (bytesRead >= totalSize) {
controller.close()
return
}
const request = controller.byobRequest
if (request) {
const size = Math.min(request.view.byteLength, totalSize - bytesRead)
bytesRead += size
request.respond(size)
}
},
})
const reader = stream.getReader({ mode: 'byob' })
let buffer = new Uint8Array(64 * 1024)
let count = 0
while (true) {
const { done, value } = await reader.read(buffer)
if (done) break
count++
// 不保留数据,复用缓冲区
buffer = new Uint8Array(64 * 1024)
}
const after = getMemoryUsage()
log('memoryLog', `读取后内存: ${after}MB`)
log('memoryLog', `内存增长: ${(after - before).toFixed(2)}MB`)
log('memoryLog', `处理块数: ${count}`)
}
function getMemoryUsage() {
if (performance.memory) {
return (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2)
}
return 0
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319